package cn.ibizlab.util.aspect;

import cn.ibizlab.util.annotation.RepeatRequest;
import cn.ibizlab.util.domain.EntityBase;
import cn.ibizlab.util.errors.InternalServerErrorException;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;


/**
 * 重复请求校验切面：对调用的目标方法进行重复请求校验，当进入方法前，将目标方法放入处理中队列中，在处理队列中的方法不允许再次访问，处理完成后将目标方法移除处理对队列（处理中队列通过redis实现）
 * 目标方法执行完成后，会自动从处理中队列中移除。为了防止服务中途出现，导致目标方法未能从处理中队列中清除，因此为处理中队列设置了缓存有效期，到达有效期将会自动从队列中清除（缓存有效期通过repeatRequest.timeout设置）
 */
@Component
@Aspect
@Order(10)
@Slf4j
@ConditionalOnExpression("'${ibiz.repeatRequestCheck:true}'.equals('true')")
public class RepeatRequestAspect {

    @Autowired
    RedisTemplate redisTemplate;
    private static final String REQUEST_SUFFIX = "ibzweb_repeatRequest";
    private static final String PROCESSING = "processing";
    private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
    private final ExpressionParser parser = new SpelExpressionParser();

    @Around("@annotation(cn.ibizlab.util.annotation.RepeatRequest)")
    public Object check(ProceedingJoinPoint point) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        Object args[] = point.getArgs();
        if (!ObjectUtils.isEmpty(args) && args[0] instanceof EntityBase) {
            EntityBase entity = (EntityBase) args[0];
            Method method = ((MethodSignature) point.getSignature()).getMethod();
            RepeatRequest repeatRequest = method.getAnnotation(RepeatRequest.class);
            Object entityKey = getEntityKey(repeatRequest.key(), method, args);
            String cacheKey = String.format("%1$s:%2$s:%3$s:%4$s", REQUEST_SUFFIX, request.getRequestURI(), entityKey, DigestUtils.md5DigestAsHex(JSON.toJSONBytes(entity)));

            //判断用户访问接口是否在处理队列中，存在，则拒绝再次访问；不存在，则将允许访问，并将接口加入处理队列中
            if (!redisTemplate.opsForValue().setIfAbsent(cacheKey, PROCESSING, repeatRequest.timeout(), TimeUnit.MILLISECONDS))
                throw new InternalServerErrorException(repeatRequest.message());

            Object result;
            try {
                //用户访问目标接口
                result = point.proceed();
            } finally {
                //成功访问目标接口后，将接口从处理队列清除，用户可再次访问
                redisTemplate.opsForValue().getOperations().delete(cacheKey);
            }
            return result;
        }
        return point.proceed();
    }

    /**
     * 获取实体数据key
     * @param expression
     * @param method
     * @param args
     * @return
     */
    protected Object getEntityKey(String expression, Method method, Object args[]) {
        Object id = null;
        try {
            if(!ObjectUtils.isEmpty(expression)){
                Expression oldExp = parser.parseExpression(expression);
                EvaluationContext context = new MethodBasedEvaluationContext(TypedValue.NULL, method, args, parameterNameDiscoverer);
                id = oldExp.getValue(context);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return id;
    }

}
